﻿using System;
using System.Collections.Specialized;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Timers;
using System.Web;
using System.Web.Configuration;
using System.Web.Hosting;
using System.Web.SessionState;

namespace EfSessionStateProvider
{
    public class SessionStateProvider : SessionStateStoreProviderBase
    {
        private SessionStateSection _config;
        private SessionStateContext _db;
        private Timer _cleanupTimer;

        public string ApplicationName { get; private set; }

        public TimeSpan SessionTimeout { get; private set; }

        public override void Initialize(string name, NameValueCollection config)
        {
            if (string.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "EntityFramework Session State Store Provider");
            }

            if (string.IsNullOrEmpty(name))
            {
                name = "EfSessionStateProvider";
            }

            base.Initialize(name, config);

            this.ApplicationName = System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath;

            System.Configuration.Configuration webCfg = WebConfigurationManager.OpenWebConfiguration(
                HostingEnvironment.ApplicationVirtualPath);
            this._config = (SessionStateSection)webCfg.GetSection("system.web/sessionState");
            this.SessionTimeout = _config.Timeout;
            var connectionStringSettings = ConfigurationManager.ConnectionStrings[config["connectionStringName"]];
            this._db = new SessionStateContext(connectionStringSettings.ConnectionString);
            _cleanupTimer = new Timer(_config.Timeout.TotalMilliseconds);
            _cleanupTimer.Elapsed += (sender, e) => CleanUpExpiredData();
            _cleanupTimer.Start();
        }

        public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout)
        {
            return new SessionStateStoreData(new SessionStateItemCollection(),
                SessionStateUtility.GetSessionStaticObjects(context),
                timeout);
        }

        public override void CreateUninitializedItem(HttpContext context, string id, int timeout)
        {
            var now = GetUtcNow();
            if (_db.Sessions.Any(o => o.ApplicationName == this.ApplicationName && o.SessionId == id && o.Expires < now))
            {
                System.Diagnostics.Debug.WriteLine("{0} has Expred!", id);
            }
            var model = new ASPStateTempSession
            {
                ApplicationName = this.ApplicationName,
                SessionId = id,
                Timeout = timeout,
                Created = now,
                Expires = now.AddMinutes(timeout),
                LockDate = now,
                LockId = 0,
                Locked = false,
                SessionItems = "",
                Flags = 1
            };
            _db.Sessions.Add(model);
            _db.SaveChanges();
        }

        public override void Dispose()
        {
            _cleanupTimer.Stop();
            _db.Dispose();
        }

        public override void EndRequest(HttpContext context)
        {
        }

        public override SessionStateStoreData GetItem(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
        {
            return GetSessionStoreItem(false, context, id, out locked, out lockAge, out lockId, out actions);
        }

        public override SessionStateStoreData GetItemExclusive(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions)
        {
            return GetSessionStoreItem(true, context, id, out locked, out lockAge, out lockId, out actions);
        }

        public override void InitializeRequest(HttpContext context)
        {
        }

        public override void ReleaseItemExclusive(HttpContext context, string id, object lockId)
        {
            var now = GetUtcNow();
            var intLockId = (int)lockId;
            var model = _db.Sessions.FirstOrDefault(o => o.ApplicationName == this.ApplicationName && o.SessionId == id && o.LockId == intLockId);
            if (model != null)
            {
                model.Locked = false;
                model.Expires = now.AddMinutes(_config.Timeout.TotalMinutes);
                _db.SaveChanges();
            }
        }

        public override void RemoveItem(HttpContext context, string id, object lockId, SessionStateStoreData item)
        {
            var intLockId = (int)lockId;
            var model = _db.Sessions.FirstOrDefault(o => o.ApplicationName == this.ApplicationName && o.SessionId == id && o.LockId == intLockId);
            if (model != null)
            {
                _db.Sessions.Remove(model);
                _db.SaveChanges();
            }
        }

        public override void ResetItemTimeout(HttpContext context, string id)
        {
            var now = GetUtcNow();
            var model = _db.Sessions.FirstOrDefault(o => o.ApplicationName == this.ApplicationName && o.SessionId == id);
            if (model != null)
            {
                model.Expires = now.AddMinutes(_config.Timeout.TotalMinutes);
                _db.SaveChanges();
            }
        }

        public override void SetAndReleaseItemExclusive(HttpContext context, string id, SessionStateStoreData item, object lockId, bool newItem)
        {
            var now = GetUtcNow();
            string sessItems = Serialize((SessionStateItemCollection)item.Items);

            if (newItem)
            {
                var old = _db.Sessions.FirstOrDefault(o => o.ApplicationName == this.ApplicationName && o.SessionId == id && o.Expires < now);
                if (old != null)
                {
                    _db.Sessions.Remove(old);
                    _db.SaveChanges();
                }
                var model = new ASPStateTempSession
                {
                    ApplicationName = this.ApplicationName,
                    SessionId = id,
                    Timeout = item.Timeout,
                    Created = now,
                    Expires = now.AddMinutes(item.Timeout),
                    LockDate = now,
                    LockId = 0,
                    Locked = false,
                    SessionItems = sessItems,
                    Flags = 0
                };
                _db.Sessions.Add(model);
                _db.SaveChanges();
            }
            else
            {
                var intLockId = (int)lockId;
                var model = _db.Sessions.FirstOrDefault(o => o.ApplicationName == this.ApplicationName && o.SessionId == id && o.LockId == intLockId);
                if (model != null)
                {
                    model.Locked = false;
                    model.Expires = now.AddMinutes(_config.Timeout.TotalMinutes);
                    model.SessionItems = sessItems;
                    _db.SaveChanges();
                }
            }
        }

        public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback)
        {
            return false;
        }

        #region private

        private SessionStateStoreData GetSessionStoreItem(bool lockRecord,
          HttpContext context,
          string id,
          out bool locked,
          out TimeSpan lockAge,
          out object lockId,
          out SessionStateActions actionFlags)
        {
            SessionStateStoreData item = null;
            lockAge = TimeSpan.Zero;
            lockId = null;
            locked = false;
            actionFlags = 0;

            string serializedItems = "";
            bool foundRecord = false;
            bool deleteData = false;
            int timeout = 0;
            var now = GetUtcNow();
            try
            {
                var model = _db.Sessions.FirstOrDefault(o => o.ApplicationName == this.ApplicationName && o.SessionId == id);

                if (lockRecord)
                {
                    if (model != null && !model.Locked && model.Expires > now)
                    {
                        model.Locked = locked;
                        model.LockDate = now;
                        if (_db.SaveChanges() == 0)
                        {
                            locked = true;
                        }
                    }
                    else
                    {
                        locked = true;
                    }
                }

                if (model != null)
                {
                    if (model.Expires < now)
                    {
                        locked = false;
                        deleteData = true;
                    }
                    else
                    {
                        foundRecord = true;
                    }

                    serializedItems = model.SessionItems;
                    lockId = model.LockId;
                    lockAge = now.Subtract(model.LockDate);
                    actionFlags = (SessionStateActions)model.Flags;
                    timeout = model.Timeout;
                }

                if (deleteData)
                {
                    _db.Sessions.Remove(model);
                    _db.SaveChanges();
                }

                if (!foundRecord)
                    locked = false;

                if (foundRecord && !locked)
                {
                    model.LockId = (int)lockId + 1;
                    model.Flags = 0;
                    _db.SaveChanges();

                    item = actionFlags == SessionStateActions.InitializeItem
                        ? CreateNewStoreData(context, (int)_config.Timeout.TotalMinutes)
                        : Deserialize(context, serializedItems, timeout);
                }
            }
            catch (Exception)
            {
                throw;
            }

            return item;
        }

        private string Serialize(SessionStateItemCollection items)
        {
            using (var ms = new MemoryStream())
            using (var writer = new BinaryWriter(ms))
            {
                if (items != null)
                    items.Serialize(writer);

                writer.Close();

                return Convert.ToBase64String(ms.ToArray());
            }
        }

        private SessionStateStoreData Deserialize(HttpContext context, string serializedItems, int timeout)
        {
            using (var ms =
              new MemoryStream(Convert.FromBase64String(serializedItems)))
            {
                var sessionItems =
                  new SessionStateItemCollection();

                if (ms.Length > 0)
                {
                    using (var reader = new BinaryReader(ms))
                    {
                        sessionItems = SessionStateItemCollection.Deserialize(reader);
                    }
                }

                return new SessionStateStoreData(sessionItems,
                  SessionStateUtility.GetSessionStaticObjects(context),
                  timeout);
            }
        }

        private DateTime GetUtcNow()
        {
            return DateTime.UtcNow;
        }

        private void CleanUpExpiredData()
        {
            var now = this.GetUtcNow();
            var list = _db.Sessions.Where(o => o.Expires < now).ToList();
            for (int i = 0; i < list.Count; i++)
            {
                _db.Sessions.Remove(list[i]);
            }
            _db.SaveChanges();
        }

        #endregion private
    }
}